#include <iostream>
#include <fstream>
#include <sstream>
#include <cstring>
#include <filesystem>
#include "tinyobj.h"
#include "tinyply.h"
#include "Model.h"

namespace pwp {

struct Buffer {
  char* data_ptr() { return buff.data(); }
  int size() { return buff.size(); }
  std::vector<char> buff;

  int Load(const std::string& path) {
    return Load(path.c_str());
  }

  int Load(const char* path) {
    FILE* fp = fopen(path, "rb");
    if (!fp) {
      printf("open %s error\n", path);
      return -1;
    }

    fseek(fp, 0, SEEK_END);
    buff.resize(ftell(fp));
    auto file_size = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    buff.resize(file_size);

    fread(data_ptr(), file_size, 1, fp);
    fclose(fp);

    return file_size;
  }
};

Model::Model(void)
{

}

Model::Model(const char* data_ptr, int data_size, ObjFormat fmt, float scale) {
  if (FMT_OBJ == fmt) {
    faceCount = createFromObjData(data_ptr, data_size, scale);
  }
  else if (FMT_PLY == fmt) {
    faceCount = createFromPlyData(data_ptr, data_size, scale);
  }
  else {
    std::cerr << "Error: ONLY support wavefront obj & ply model file" << std::endl;
  }
}

Model::Model(std::string fileName, float scale) {
  Buffer buff;
  buff.Load(fileName);
  auto data_ptr = buff.data_ptr();
  auto data_size = buff.size();

  std::filesystem::path file_path(fileName);
  if (file_path.extension() == std::string(".obj")) {
    faceCount = createFromObjData(data_ptr, data_size, scale);
  }
  else if (file_path.extension() == std::string(".ply")) {
    faceCount = createFromPlyData(data_ptr, data_size, scale);
  }
  else {
    std::cerr << "ERROR: INVALID model file" << std::endl;
  }
}

struct membuf : std::streambuf {
  membuf(char* base, std::ptrdiff_t n) {
    this->setg(base, base, base + n);
  }

  pos_type seekpos(pos_type sp, std::ios_base::openmode which) override {
    return seekoff(sp - pos_type(off_type(0)), std::ios_base::beg, which);
  }

  pos_type seekoff(off_type off,
    std::ios_base::seekdir dir,
    std::ios_base::openmode which = std::ios_base::in) override {
    if (dir == std::ios_base::cur)
      gbump(off);
    else if (dir == std::ios_base::end)
      setg(eback(), egptr() + off, egptr());
    else if (dir == std::ios_base::beg)
      setg(eback(), eback() + off, egptr());
    return gptr() - eback();
  }
};

int Model::createFromPlyData(
  const char* data_ptr, 
  int data_size, 
  float scale) 
{
  membuf buf(const_cast<char*>(data_ptr), data_size);
  std::istream ims(&buf);

  tinyply::PlyFile file;
  file.parse_header(ims);

  std::shared_ptr<tinyply::PlyData> ply_verts;
  ply_verts = file.request_properties_from_element("vertex", { "x", "y", "z" });
  std::shared_ptr<tinyply::PlyData> ply_faces;
  ply_faces = file.request_properties_from_element("face", { "vertex_indices" }, 3);
  file.read(ims);

  struct float3 { float x, y, z; };
  std::vector<float3> verts;
  const size_t num_vertices_bytes = ply_verts->buffer.size_bytes();
  verts.resize(ply_verts->count);
  std::memcpy(verts.data(), ply_verts->buffer.get(), num_vertices_bytes);

  struct uint3 { uint32_t x, y, z; };
  std::vector<uint3> faces;
  const size_t num_faces_bytes = ply_faces->buffer.size_bytes();
  faces.resize(ply_faces->count);
  std::memcpy(faces.data(), ply_faces->buffer.get(), num_faces_bytes);

  for (auto& vert : verts) {
    auto x = vert.x * scale;
    auto y = vert.y * scale;
    auto z = vert.z * scale;
    vertices.push_back(ModelVertex(x, y, z));
  }

  int faceCount = 0;
  size_t index_offset = 0;
  for (size_t fid = 0; fid < faces.size(); ++fid) {
    ModelFace face;

    face.verticesVector[0] = faces[fid].x;
    face.verticesVector[1] = faces[fid].y;
    face.verticesVector[2] = faces[fid].z;

    this->faces.push_back(face);

    faceCount++;
  }

  verticesVector.resize(vertices.size() * 4);
  
  this->minZ = vertices[0].vector3d.z;
  for (size_t i = 0, j = 0; i < vertices.size(); i++, j += 4) {
    verticesVector[j] = vertices[i].vector3d.x;
    verticesVector[j + 1] = vertices[i].vector3d.y;
    verticesVector[j + 2] = vertices[i].vector3d.z;

    if (this->minZ < vertices[i].vector3d.z)
      this->minZ = vertices[i].vector3d.z;

    verticesVector[j + 3] = 1;
  }

  float inf = std::numeric_limits<float>::infinity();
  lbn = VECTOR3DA(inf, inf, inf);
  rtf = VECTOR3DA(-inf, -inf, -inf);
  for (size_t i = 0, j = 0; i < vertices.size(); i++, j += 4) {
    auto pt = vertices[i].vector3d;

    if (pt.x < lbn.x) lbn.x = pt.x;
    if (pt.y < lbn.y) lbn.y = pt.y;
    if (pt.z < lbn.z) lbn.z = pt.z;
    if (pt.x > rtf.x) rtf.x = pt.x;
    if (pt.y > rtf.y) rtf.y = pt.y;
    if (pt.z > rtf.z) rtf.z = pt.z;
  }

  return faceCount;
}

int Model::createFromObjData(
  const char* data_ptr, 
  int data_size, 
  float scale)
{
  tinyobj::attrib_t attributes;
  std::vector<tinyobj::shape_t> shapes;
  std::vector<tinyobj::material_t> materials;
  std::string warning;
  std::string error;

  std::stringstream stm(data_ptr);
  if (!tinyobj::LoadObj(
    &attributes, &shapes, &materials, &warning, &error, &stm, nullptr, true, false))
  {
    std::cerr << "TinyObjLoader failed to load data from buffer " << std::endl;
    return false;
  }

  vertices.clear();
  for (int i = 0; i < attributes.vertices.size(); i += 3) {
    auto x = attributes.vertices[i] * scale;
    auto y = attributes.vertices[i + 1] * scale;
    auto z = attributes.vertices[i + 2] * scale;
    vertices.push_back(ModelVertex(x, y, z));
  }

  faces.clear();
  int faceCount = 0;
  for (const auto& shape : shapes) {
    size_t index_offset = 0;
    for (size_t f = 0; f < shape.mesh.num_face_vertices.size(); ++f) {
      if (shape.mesh.num_face_vertices[f] != 3) {
        std::cerr << "Mesh contains non triangle shapes" << std::endl;
        index_offset += shape.mesh.num_face_vertices[f];
        continue;
      }

      ModelFace face;
      face.verticesVector[0] = shape.mesh.indices[index_offset].vertex_index;
      face.verticesVector[1] = shape.mesh.indices[index_offset + 1].vertex_index;
      face.verticesVector[2] = shape.mesh.indices[index_offset + 2].vertex_index;
      faces.push_back(face);

      faceCount++;
      index_offset += 3;
    }
  }

  verticesVector.resize(vertices.size() * 4);
  this->minZ = vertices[0].vector3d.z;
  for (size_t i = 0, j = 0; i < vertices.size(); i++, j += 4) {
    verticesVector[j] = vertices[i].vector3d.x;
    verticesVector[j + 1] = vertices[i].vector3d.y;
    verticesVector[j + 2] = vertices[i].vector3d.z;

    if (this->minZ < vertices[i].vector3d.z)
      this->minZ = vertices[i].vector3d.z;

    verticesVector[j + 3] = 1;
  }

  float inf = std::numeric_limits<float>::infinity();
  lbn = VECTOR3DA(inf, inf, inf);
  rtf = VECTOR3DA(-inf, -inf, -inf);
  for (size_t i = 0, j = 0; i < vertices.size(); i++, j += 4) {
    auto pt = vertices[i].vector3d;

    if (pt.x < lbn.x) lbn.x = pt.x;
    if (pt.y < lbn.y) lbn.y = pt.y;
    if (pt.z < lbn.z) lbn.z = pt.z;
    if (pt.x > rtf.x) rtf.x = pt.x;
    if (pt.y > rtf.y) rtf.y = pt.y;
    if (pt.z > rtf.z) rtf.z = pt.z;
  }

  return faceCount;
}

void Model::ToModelH(ModelH* newModel)
{
  newModel->originalVerticesVector = verticesVector.data();
}


void Model::ToModelHInit(ModelH* newModel) {
  newModel->isAllocated = true;

  newModel->faces = &this->faces;
  newModel->verticesVectorSize = this->vertices.size();
  newModel->verticesVector = new VFLOAT[newModel->verticesVectorSize * 4];
}

}